package mbsl.commons.base;

import java.io.File;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableColumn;
import org.eclipse.swt.widgets.TableItem;

import mbsl.commons.Commons;
import mbsl.commons.lib.BasicElement;
import mbsl.commons.lib.Element;
import mbsl.commons.lib.Location;
import mbsl.main.MainController;
import mbsl.main.StatusBarController;
import mbsl.main.StatusBarController.Status;
import mbsl.storage.Database;
import mbsl.system.Core;
import mbsl.system.Logger;
import mbsl.system.TextProvider;
import mbsl.ui.dialogs.Dialogs;

/**
 * A generic element controller.
 * <p>
 * This class is constructed to work with the ElementPanel, leaving the extending classes to deal mostly with dialogs 
 * and such.  The control of flow should also be kept in this class as much as possible, and have its children answer
 * to calls.  The class is very complex (and possibly obese), but hopefully the methods will have sufficient 
 * documentation to explain what their meaning in life is.
 */
public abstract class ElementController<T extends Element<T>> {
	// CONSTRUCTORS AND SETUP
	protected ElementController(MainController pParent, Class<T> pControllerClass, String pLabelText) {
		mPanel = new ElementPanel(pParent.getTabFolder(), pLabelText);
		mColumns = new ArrayList<Column>();

		mPanel.addTabFocusListener(new TabFocusListener() {
			@Override
			public void focusGained() {
				ElementController.this.focusGained();
			}
			@Override
			public void focusLost() {
				ElementController.this.focusLost();
			}
		});

		if (COLUMN_INDEX_OFFSET > 0)
			addInvisibleColumn(); // Needed because of the bugs with the first column.
		
		mParent = pParent;
		mControllerClass = pControllerClass;
		mStatusBar = mParent.getStatusBarController();
		mElementList = Collections.emptyList();
		
		setupPanel();

		initColumns();
		
		if (pParent.hasFocus(getPanel().getTabItem()))
			focusGained();
	}
	/**
	 * Returns the parent controller
	 */
	protected MainController getParent() {
		return mParent;
	}
	/**
	 * Returns the Core this controller is working for.
	 */
	public Core getCore() {
		return mParent.getCore();
	}
	/**
	 * Called when the tab gains focus.
	 */
	protected void focusGained() {
		mStatusBar.finishAndRelease(Commons.tp.get("status.listDefault"), mElementList.size());
		
		mStatusBar.requestOwnership(true);
	}
	/**
	 * Returns the status bar controller the element controller holds.
	 */
	protected StatusBarController getStatusBar() {
		return mStatusBar;
	}
	/**
	 * Called when the tab loses focus.
	 */
	protected void focusLost() {
		mStatusBar.release();
	}
	/**
	 * Sets up the ElementPanel
	 */
	private void setupPanel() {
		getPanel().getSearchPanel().addListener(new Listener() {
			@Override
			public void handleEvent(Event pEvent) {
				search(getPanel().getSearchPanel().getText());
			}
		});
		getPanel().addNewButtonListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent pE) {
				newDialog();
			}
		});
		getPanel().addViewButtonListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent pE) {
				if (getPanel().getTable().getSelectionCount() > 0)
					view(getPanel().getTable().getSelectionIndex());
			}
		});
		getPanel().addPopUpDeleteListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent pE) {
				delete(getPanel().getTable().getSelectionIndices());
			}
		});
		getPanel().addPopUpEditListener(new SelectionAdapter() {
			@Override
			public void widgetSelected(SelectionEvent pE) {
				editDialogs(getPanel().getTable().getSelectionIndices());
			}
		});
	}
	
	/**
	 * Returns a new instance of the controller class.
	 * @return
	 */
	protected abstract T getNewInstance();

	
	// ORDERING AND SEARCHING
	/**
	 * Sets the order arguments to the passed string.  The string should usually start with <i>order by</i> and must be valid HQL.
	 * @param pArguments The order arguments in HQL.
	 * @param pDoRefresh Whether to refresh after the arguments are set.
	 */
	protected void setOrderArguments(String pArguments, boolean pDoRefresh) {
		mOrderArguments = pArguments;
		
		if (pDoRefresh)
			refresh();
	}
	/**
	 * Searches for the provided string.  % may be used as a wild card, and 
	 * <p>
	 * Note: This will do a refresh automatically.
	 * <p>
	 * Note that subsequent calls with the same string will not refresh the table.  Use 
	 * {@link #refresh()} in these cases.
	 * @param pFilterText The string to search for.
	 */
	protected void search(String pFilterText) {
		String oldSearchString = mSearchString;

		if (pFilterText == null || pFilterText.isEmpty())
			mSearchString = null;
		else
			mSearchString = String.format(getSearchQuery(), pFilterText.toLowerCase());

		/* We'll refresh if:
		 *  1. Exactly one of the old or the new search strings are null, or
		 *  2. both are non-null, and they differ in content.
		 * 
		 * Otherwise there's no need to. 
		 */// ^ is the XOR operator
		if ((oldSearchString == null ^ mSearchString == null) || (oldSearchString != null && mSearchString != null && !mSearchString.equals(oldSearchString)))
			refresh();
	}
	/**
	 * The HQL query used when doing searches.  Insert %s (or %1$s) where the search string should go.
	 */
	abstract protected String getSearchQuery();
	
	// TABLE CREATION, INTERACTION AND CONTROL
	/**
	 * Refreshes the table model, effectively removing all elements, and putting in new ones.
	 * @see #refresh(Element)
	 * @see #refresh(int)
	 */
	protected void refresh() {
		mStatusBar.setProgress(0);
		StringBuilder hql = new StringBuilder(
				(mSearchString == null ? 0 : mSearchString.length()) + 
				(mOrderArguments == null ? 0 : mOrderArguments.length()) +
				1
		);

		if (mSearchString != null)
			hql.append(mSearchString);
		
		if (mOrderArguments != null) {
			if (hql.length() > 0)
				hql.append(' ');
			
			hql.append(mOrderArguments);
		}
		
		mStatusBar.setProgress(5);
		logger.debug("Refreshed with arguments [%s].", hql);
		int oldSize = mElementList.size();
		
		mElementList = getDatabase().getList(mControllerClass, hql.toString());
		
		if (oldSize / 2 > mElementList.size())
			getPanel().getTable().removeAll();
			
		
		mStatusBar.setProgress(20);
		int i;

		Object[] rowData = new Object[getColumnCount()];
		
		for (i = 0; i < mElementList.size(); i++) {
			mStatusBar.setProgress(20 + i / mElementList.size() * 80 / 100);

			for (int j = 0; j < rowData.length; j++)
				rowData[j] = null;
			
			T element = mElementList.get(i);
			
			TableItem item = new TableItem(getPanel().getTable(), SWT.None, i);
			
			fillRow(item, rowData, element);
			
			addRowData(item, rowData);
			
			item.setData(TABLEITEMDATA_ELEMENT, element);

			if (getPanel().getTable().getItemCount() > i + 1)
				getPanel().getTable().remove(i + 1);
		}

		if (getPanel().getTable().getItemCount() > mElementList.size())
			getPanel().getTable().remove(i, getPanel().getTable().getItemCount() - 1);

		mStatusBar.setInfo(getDefaultStatus());
		mStatusBar.setFinished();
	}
	/**
	 * Refreshes the passed row.
	 */
	protected void refresh(int pRow) {
		if (pRow < 0 || pRow >= getPanel().getTable().getItemCount())
			throw new IllegalArgumentException("Row number out of bounds");
		
		TableItem item = getPanel().getTable().getItem(pRow);
		T element = mElementList.get(pRow);
		
		fillRow(item, new Object[getPanel().getTable().getColumnCount()], element);
	}
	/** Refreshes the row associated with the passed Element. */
	protected void refresh(T pElement) {
		refresh(getElementRow(pElement));
	}
	/** Returns the row at which the passed Element is described. */
	protected int getElementRow(T pElement) {
		return mElementList.indexOf(pElement);
	}
	/**
	 * Fills row data array using the provided object.
	 * <p>
	 * The object array is the size of the amount of columns.  Any object can be set as the value, but only Images get special treatment.  All others are applied toString() to.  Null is also allowed to signal an empty cell.
	 * @param tableItem The TableItem to fill.
	 * @param object The object to use when filling the TableItem.
	 */
	protected abstract void fillRow(TableItem tableItem, Object[] rowData, T object);
	/**
	 * Returns the Element at the provided index.
	 */
	protected T getElementAtIndex(int pIndex) {
		return mElementList.get(pIndex);
	}
	/** 
	 * Delete the items at the provided indices 
	 */
	protected void delete(int[] pTableIndices) {
		StatusBarController sbc = mParent.getStatusBarController();
		TextProvider tp = Commons.tp.prefix("common.delete");
		
		sbc.requestOwnership(mStatusBar);
		sbc.setStatus(Status.Working);
		
		if (pTableIndices.length > 1) { // We're not asking if it's just one.
			sbc.setInfo(tp.get("task"), pTableIndices.length);

			StringBuilder message = new StringBuilder(String.format(tp.get("selection.pre"), pTableIndices.length));
			
			for (int i = 0; i < pTableIndices.length && i < DELETE_DIALOG_ROWS; i++)
				if (i < DELETE_DIALOG_ROWS - 1 || pTableIndices.length == DELETE_DIALOG_ROWS)
					message.append(String.format(tp.get("selection.list"), getElementAtIndex(pTableIndices[i]).getName()));
				else
					message.append(String.format(tp.get("selection.post"), pTableIndices.length - i));
			
			int option = Dialogs.question(mParent.getWindow().getShell(), message.toString(), String.format(tp.get("confirm"), pTableIndices.length));
			
			if (option != SWT.YES) {
				sbc.finishAndRelease(tp.get("abort"));
				return;
			}
		}
		// Well, we've gotten the green light, so let's go.
		String name = null;

		for (int index : pTableIndices) {
			Element<T> element = getElementAtIndex(index);
			
			logger.debug(String.format("Sent delete for object [%s]", element));
			
			if (element.getChildren() != null) { // If it has any children, we'd better unattach them before we move on
				for (Element<T> child : element.getChildren()) {
					child.setParent(null);
					
					getDatabase().save(child);
				}
			}
				
			if (pTableIndices.length == 1)
				name = getElementAtIndex(index).getName();
		
			getDatabase().delete(element);
		}
		
		getPanel().getTable().remove(pTableIndices);
		
		for (int i = 0; i < pTableIndices.length; i++) {
			int index = pTableIndices[pTableIndices.length - 1 - i];
			
			mElementList.remove(index);
		}
		
		if (pTableIndices.length == 1)
			sbc.finishAndRelease(tp.get("done.single"), name);
		else
			sbc.finishAndRelease(tp.get("done.multi"), pTableIndices.length);
	}

	/**
	 * This class holds a table column and its actions.
	 * <p>
	 * A new instance of this class will automatically add itself to the column list.
	 */
	protected class Column {
		/**
		 * Creates a column and adds it to the column list.
		 * 
		 * @param pIndex
		 *            The index of the column.
		 * @param pName
		 *            The name of the column.
		 * @param pWidth
		 *            The width of the column.
		 * @param pOrderString
		 *            The ordering string this column will use when the table is
		 *            sorted by it. Needs to be a formatable string with %s
		 *            where asc/desc should go.
		 * @param pDefaultOrder
		 *            The default sorting order. Probably one of <i>asc</i> or
		 *            <i>desc</i>.
		 */
		public Column(int pIndex, String pName, int pWidth, String pOrderString, Order pDefaultOrder) {
			mTableColumn = new TableColumn(getPanel().getTable(), SWT.NONE, pIndex + COLUMN_INDEX_OFFSET);
			mOrder = pDefaultOrder;

			mTableColumn.setText(pName);
			mTableColumn.setWidth(pWidth);
			mTableColumn.addSelectionListener(new SelectionAdapter() {
				public void widgetSelected(SelectionEvent e) {
					activate();
				}
			});
			
			mColumns.add(pIndex + COLUMN_INDEX_OFFSET, this);

			mOrderString = pOrderString;
		}
		
		private void activate() {
			Table table = getPanel().getTable();
			
			if (table.getSortColumn() != mTableColumn) {
				table.setSortColumn(mTableColumn);
			} else {
				if (mOrder == Order.asc)
					mOrder = Order.desc;
				else
					mOrder = Order.asc;
			}
			
			table.setSortDirection(mOrder.getValue());
				
			refresh(mOrder);
		}
		
		protected void refresh(Order pOrder) {
			ElementController.this.setOrderArguments(getOrderString(pOrder), true);
		}

		private String getOrderString(Order pOrder) {
			return String.format(mOrderString, pOrder.name());
		}
		public TableColumn getTableColumn() {
			return mTableColumn;
		}
		protected Order getOrder() {
			return mOrder;
		}
		
		private TableColumn mTableColumn;
		private Order mOrder;
		private String mOrderString;
	}

	
	// UI INTERACTION
	/**
	 * Imports the names from a folder and shows New dialogs for them.
	 */
	public void importFolder() {
		StatusBarController statusBarController = mParent.getStatusBarController();
		TextProvider tp = Commons.tp.prefix("common.import");

		statusBarController.requestOwnership(mStatusBar);

		statusBarController.setInfo(tp.get("loading"));
		statusBarController.setStatus(Status.Working);

		String folderPath = Dialogs.folder(mParent.getWindow().getShell(), tp.get("dialog.description"), tp.get("dialog.title"));
		
		if (folderPath == null) {
			statusBarController.finishAndRelease(tp.get("cancel"));
			return;
		}
		
		File folder = new File(folderPath);
		
		if (!folder.canRead()) {
			statusBarController.finishAndRelease(tp.get("inputError"));
			return;
		} else if (!folder.isDirectory()) {
			statusBarController.finishAndRelease(tp.get("folderError"));
			return;
		}

		File[] folders = folder.listFiles(new FileFilter() {
			public boolean accept(File pFile) {
				return pFile.isDirectory();
			}
		});
		
		statusBarController.setInfo(tp.get("task"), folders.length);

		for (int i = 0; i < folders.length; i++) {
			statusBarController.setProgress(i, folders.length);
			String name = folders[i].getName();
			
			T element = getNewInstance();
			
			if (element == null) {
				logger.warning("ElementController failed to obtain a new instance of %1$s.", mControllerClass.getCanonicalName());
				// Release the controller and post the error message.
				statusBarController.finishAndRelease(tp.get("loadError"), i);
				return;
			}

			element.setName(name);

			newDialog(element);
		}
		// Release the controller.
		statusBarController.finishAndRelease(getDefaultStatus());
	}
	/**
	 * Returns the String used as the default status bar message.
	 * <p>
	 * The default is "[n] Objects".
	 */
	protected String getDefaultStatus() {
		return String.format(Commons.tp.get("status.listDefault"), mElementList.size());
	}

	
	// DIALOGS
	/**
	 * Opens a New Dialog.  Convenience method for newDialog(new T()).
	 */
	protected void newDialog() {
		newDialog(getNewInstance());
	}
	/**
	 * Opens a New Dialog with the non-null fields from the supplied element filled out.
	 */
	protected abstract void newDialog(T element);
	/**
	 * Opens an Edit Dialog for each of the elements at the indices passed.
	 * <p>
	 * This is a convenience method for
	 * <pre>
	 * for (int row : pRows)
	 * 	editDialog(getObject(row));
	 * </pre>
	 * @param pRows An array of indices to open edit dialogs for.
	 */
	protected void editDialogs(int[] pRows) {
		for (int row : pRows)
			editDialog(getElementAtIndex(row));
	}
	/**
	 * Opens an Edit Dialog for the passed element.
	 */
	protected abstract void editDialog(T element);
	/**
	 * Opens a View Dialog for the element at the passed index.
	 */
	protected void view(int pIndex) {
		viewDialog(getElementAtIndex(pIndex));
	}
	/**
	 * Opens a View dialog for the passed element.
	 * <p>
	 * Users of the default implementation must override {@link #setUpViewDialog(Element)}.
	 * @see #setUpViewDialog(Element)
	 */
	protected abstract void viewDialog(final T pElement);
	
	protected void editBasicElements(Class<? extends BasicElement> pClass) {
		if (pClass.equals(Location.class)) {
			//TODO
			
			return;
		}
		
		throw new IllegalArgumentException(ElementController.class.getCanonicalName() + " has no editor for " + pClass.getCanonicalName());
	}
	
	// SYSTEM
	/**
	 * Returns the Database object provided by the Core. 
	 */
	protected Database getDatabase() {
		return mParent.getCore().getDatabase();
	}
	
	// UTILITIES
	/**
	 * Returns the ElementPanel this class controls
	 */
	protected ElementPanel getPanel() {
		return mPanel;
	}
	/**
	 * Makes the column at the passed 0-based index the active one
	 */
	protected void setActiveColumn(int pColumnIndex) {
		mColumns.get(pColumnIndex + COLUMN_INDEX_OFFSET).activate();
	}

	protected int getColumnCount() {
		return mColumns.size() - COLUMN_INDEX_OFFSET;
	}
	protected void addRowData(TableItem pItem, Object[] pObjects) {
		if (pObjects.length < mColumns.size() - COLUMN_INDEX_OFFSET)
			throw new IllegalArgumentException("Array must be the same size or larger than the number of columns.");
		
		for (int i = COLUMN_INDEX_OFFSET; i < mColumns.size(); i++) {
			if (pObjects[i - COLUMN_INDEX_OFFSET] instanceof Image)
				pItem.setImage(i, (Image)pObjects[i - COLUMN_INDEX_OFFSET]);
			else if (pObjects[i - COLUMN_INDEX_OFFSET] != null /*&& pObjects[i - COLUMN_INDEX_OFFSET].toString() != null*/)
				pItem.setText(i, pObjects[i - COLUMN_INDEX_OFFSET].toString());
		}
	}
	protected void addRowData(TableItem pItem, List<?> pObjects) {
		addRowData(pItem, pObjects.toArray());
	}
	/**
	 * Initialise the columns.
	 * <p>
	 * This method is called by the ElementController at panel setup to allow its children to add columns to the
	 * table.
	 */
	protected abstract void initColumns();
	/**
	 * The order in which items will be sorted.
	 */
	public enum Order {
		/** Ascending order */
		asc(SWT.UP), 
		/** Descending order */
		desc(SWT.DOWN);
		
		private int mValue;
		
		private Order(int pValue) {
			mValue = pValue;
		}
		/** Returns the symbol used to represent this type of order. */
		public int getValue() {
			return mValue;
		}
	}
	
	private void addInvisibleColumn() {
		TableColumn t = new TableColumn(mPanel.getTable(), SWT.NONE, 0);
		
		t.setWidth(0);
		t.setResizable(false);
		t.setMoveable(false);
		
		mColumns.add(null);
	}

	
	/** 
	 * This offset is needed because of the empty column that resides between the first visible column and the left border.
	 * See the changelog entry for 2009-06-21 for details.  When, some sunny day, the bug is fixed, simply set this to 0.
	 */
	public static final int COLUMN_INDEX_OFFSET = 1;
	private ElementPanel mPanel;
	private List<Column> mColumns;


	// CONFIGURATION
	/** The minimum rating value.  The rating range must not contain -1.*/
	public static final int RATING_MIN = 0;
	/** The maximum rating value.  See RATING_MIN for more details. */
	public static final int RATING_MAX = 5;
	/** The maximum number of object names to include in the confirm delete dialog. */
	private static final int DELETE_DIALOG_ROWS = 6;
	/** The index String used to get the element attached to each TableItem. */
	public static final String TABLEITEMDATA_ELEMENT = "element";

	private static Logger logger = new Logger(ElementController.class);
	
	/** The parent controller. Use this to contact Core. */
	private MainController mParent;
	/** The status bar controller for general usage. For complex tasks, methods should get their own from the MainController. */
	private StatusBarController mStatusBar;
	/** The Class of the Element-extending type of objects this controller (and view) uses. Needed for dynamic instancing. */
	private Class<T> mControllerClass;
	/** The list of elements currently shown in the table. */
	private List<T> mElementList;
	/** The order arguments used to generate element lists. */
	private String mOrderArguments;
	/** The search string used to generate element lists. */
	private String mSearchString;
}